Java安全[反射(1)]

“类初始化”和“类实例化”严格区分

Java安全[反射(1)]

反射的概念

反射是大多数语言不可少的一部分,

指在程序运行中,对于任何一个类,都可以通过反射拿到所有属性和方法(包含私有)

拿到的方法可以调用,总之通过反射,我们可以蒋Java这种静态的语言附加上动态的特性。

反射是指运行时检查和操作类、接口、字段、方法等程序结构的能力。

Java的反射是指程序在运行期可以拿到一个对象的所有信息。

它允许运行中的 Java 程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性和方法。

*Class对象与反射的关系

Class对象Java反射机制的入口,它封装了一个类或接口的运行时信息

通过调用Class类的方法,可以获取这些信息,包括类的构造函数、方法、属性等。

这些信息可以用来在运行时动态地创建对象、调用方法和访问字段

Class对象与Class的关系

每一个类都有一个对应的Class对象,它封装了该类的运行时信息。

Class对象与实例对象的关系

Class对象表示类运行时的信息,而实例对象则是类的一个具体实例。

每个实例对象都属于一个特定的类,并且与该类的Class对象相关联。

可以通过实例对象来获取其所属类的Class对象,也可通过Class对象来创建实例对象。

*反射例子

1
2
3
4
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}

上面的例子中,这几个方法包揽了几个在反射中极为重要的方法

  • 获取类的方法: forName

  • 实例化类对象的方法: newInstance

    从java9开始,建议使用getDeclaredConstructor().newInstance()来代替newInstance方法,因为newInstance方法会抛出不必要的

    InstantiationExceptionIllegalAccessException异常,而且它也不够清晰地表明正在调用构造函数。

  • 获得函数的方法: getMethod

  • 执行函数的方法: invoke

以上的四种方法几乎包揽所有Java安全中各种发射有关的payload


Java反射运用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.lang.reflect.Method;

public class ReflectionExample {
public static void main(String[] args) throws Exception {
// 使用 forName 方法获取类的 Class 对象
// 此处获得MyClass类的Class对象
Class<?> cls = Class.forName("MyClass");

// 使用 newInstance 方法实例化类对象
Object obj = cls.getDeclaredConstructor().newInstance();

// 获取方法
// String.class 代表参数类型为 String 的方法
Method method = cls.getMethod("sayHello", String.class);

// 执行方法
// 第一个参数为类对象,后面的参数为方法的参数
method.invoke(obj, "John");
}
}

class MyClass {
public void sayHello(String name) {
System.out.println("Hello, " + name + "!");
}
}

运行截图,获取Myclass类对象,然后调用Myclass中的方法sayHello,主函数说明参数类型String.class并传参John

其中Myclass类没有使用任何访问修饰符(如publicprivateprotected),这种访问权限的范围是限定在同一个包(package)中的其他类可以访问它。

image-20230816215846754

Myclass类可以在src目录以独立的文件格式存在Myclass.class

但是不能在src目录下存在两个相同名字的类,

  • 在反射过程中,当你尝试通过Class.forName("MyClass")这样的方式加载类时,类加载器会在类路径(包括src目录等)中查找MyClass对应的.class文件来加载,如果没有足够的信息(如包名)来区分应该加载哪一个MyClass,就会导致报错。

在进行反射时会发生报错:类重复

image-20230816205947106

获取类的三个方法

通常我们有三种方法获取一个”类”,也就是java.lang.Class对象

forName不是获取”类”的唯一途径。

obj.getClass()

如果上下文存在某个实例obj,那么可以通过obj.getClass()来获取它的类

1
2
3
4
5
6
7
8
9
10
11
public class ReflectionExample {
public static void main(String[] args) {
MyClass obj = new MyClass();
Class<?> cls = obj.getClass();
System.out.println("Class Name: " + cls.getName());
}
}

class MyClass {
// 类的定义
}

一个obj对象MyClass实例

image-20230816215442026

两个obj对象MyClassYourClass实例

image-20230816215755720

.class[不属于反射]

如果你已经加载了某个类,只是想获取它的java.lang.Class对象,那么就直接拿它的class属性即可。这个方法其实并不属于反射。

1
2
3
4
5
6
7
8
9
10
public class ReflectionExample {
public static void main(String[] args) {
Class<?> cls = Test.class;
System.out.println("Class Name: " + cls.getName());
}
}

class Test {
// 类的定义
}
image-20230816220429728

Class.forName()

如果你知道某个类的名字,想获得到这个类,就可以用forName来进行获取

1
2
3
4
5
6
7
8
9
10
11
public class ReflectionExample {
public static void main(String[] args) throws ClassNotFoundException {
String className = "MyClass";
Class<?> cls = Class.forName(className);
System.out.println("Class Name: " + cls.getName());
}
}

class MyClass {
// 类的定义
}
image-20230816220712735

反射的作用

getClass().forName("java.lang.Runtime")

在安全研究中,我们使用反射的一大目的,就是绕过某些沙盒。

比如,上下文中如果只是Integer类型的数字,我们如何获取到可以执行命令的Runtime类呢?

java.lang.Runtime 类是一个与 Java 运行时环境相关的类,它提供了访问 Java 运行时系统的接口,例如执行外部命令、管理内存等功能。这个类采用了单例模式,也就是整个 Java 应用程序在运行期间通常只有一个 Runtime 实例存在。

也许可以这样(伪代码):

1
1.getClass().forName("java.lang.Runtime")

这个代码看不懂,找了几篇文章,下面是个人理解:

这段代码是非常常见的payload格式,在很多服务器报警和CVE中常见,下面是一个对应漏洞分析文档

http://exploit-db.com/docs/english/46303-remote-code-execution-with-el-injection-vulnerabilities.pdf

常见的格式应该是这种

*.getClass().forName("java.lang.Runtime").getRuntime().exec()

简单解析一下,*为某实例化对象,getClass获取其类对象,Class.forName是对类进行加载,getRuntime可获得这个代表运行时环境的实例对象,以便后续进行如执行外部命令等操作。

文档中对该payload进行了展示(经典弹计算器

image-20230817165815554

Runtime类,执行命令的功能效果,实例代码执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.IOException;

public class CommandExecution {
public static void main(String[] args) {
try {
// 执行命令
Process process = Runtime.getRuntime().exec("calc");
// 等待命令执行完成
process.waitFor();
// 获取命令的退出值
int exitValue = process.exitValue();
System.out.println("命令执行完成,退出值:" + exitValue);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
image-20230821155641195

forName重载/加载/初始化

可以看看菜鸟教程或者其他文章

Java class.forname 详解 | 菜鸟教程 (runoob.com)

【Java核心技术】类型信息(Class对象 反射 动态代理) - luoxn28 - 博客园 (cnblogs.com)

Class.forName()用法详解_mocas_wang的博客-CSDN博客

forName有两个函数重载:

  • Class<?> forName(String name)
  • Class<?> forName(String name, boolean initialize, ClassLoader loader)

第一个就是我们最常见的获取class的方式,其实可以为第二种方式的一个封装

1
2
3
Class.forName(className)
// 等于
Class.forName(className,true,currentLoader)

默认情况下,

forName的第一个参数是类名

forName的第二参数表示是否初始化

第二个参数initialize常常被人误解,使用功能”.class”来创建Class对象的引用时

.classforName()在Java中的区别是:

  • .class是在编译时确定的,不会触发类的初始化,只是获取类的Class对象
  • forName()是在运行时动态加载类,并且会触发类的初始化,除非指定initializefalse

p.s.初始化类和创建类对象的区别

初始化类和创建类对象的区别是:

  • 初始化类是指加载类的字节码到内存中,并执行类的静态变量静态代码块。初始化类只会发生一次,仅仅当类被首次使用时。
  • 创建类对象是指使用类的构造方法来分配内存空间,并给对象的属性赋值。创建类对象可以发生多次,每次都会返回一个新的对象

所以第二参数即使为initialize=true,这里指的也是初始化类而非创建类对象,所以不会触发构造函数

forName()

通过这个例子,

可以看到初始化的作用和创建类对象,

第一次初始化时,加载静态变量静态代码块,打印数据

1
2
2
static block

第二次初始化时,不加载静态变量静态代码块,无任何打印数据

创建类对象时,加载类中构造函数,如果多次创建类对象,会执行多次构造函数

image-20230821212351622.png

.class

可以看到.class不会进行初始化静态变量静态代码块,也不会创建类对象

image-20230822092726107

常见有三种初始化

1
2
3
4
5
6
7
8
9
10
11
public class TrainPrint {
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class);
}
public TrainPrint() {
System.out.printf("Initial %s\n", this.getClass());
}
}

这三种中,

最先调用的是static{}

然后是单个中{}

最后是构造函数

运行便可知,

image-20230822212911537

因为static{}是类初始化时调用的,而{}中代码是放在构造函数super()后面,也就是构造函数的前面

==>

forNameinitialize=true其实就是告诉Java虚拟机是否执行类初始化

恶意调用

假如我们有如下函数,其中函数的参数name可控:

1
2
3
public void ref(String name) throws Exception {
Class.forName(name);
}

我们再构造一个恶意类,将恶意的代码放置在static{}中,从而执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.Runtime;
import java.lang.Process;
public class EvilClass {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}

当然,这个恶意类如何带入目标机器中,可能就涉及到ClassLoader的一些利用方法

后面学到会补充这里实现传入的过程

forName的第三个参数ClassLoader

ClassLoader是什么呢?

它就是一个”加载器”,告诉Java虚拟机如何加载这个类。

Java中默认的ClassLoader就是根据类名来加载类,这个类名是类的完整路径,如java.lang.Runtime